package com.googlecode.jch.plugin.submission;

import com.googlecode.jch.Solution;
import com.googlecode.jch.SolutionConfig;
import com.googlecode.jch.plugin.AbstractActionOverSolution;
import com.googlecode.jch.plugin.JCHUtil;
import com.googlecode.jch.plugin.configuration.JCHConfiguration;
import com.googlecode.jch.plugin.templates.JCHFileTemplateProvider;
import com.googlecode.jch.sample.SimpleInputReader;
import com.intellij.ide.fileTemplates.FileTemplate;
import com.intellij.ide.fileTemplates.FileTemplateManager;
import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.ide.util.DirectoryUtil;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.wm.ToolWindowId;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.search.GlobalSearchScope;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.*;

/**
 * @author Iskander Akishev
 */
public class PrepareForSubmissionAction extends AbstractActionOverSolution {
	private static final String MAIN_CLASS_NAME = "Main";
	private static final String MAIN_CLASS_FILE_NAME = MAIN_CLASS_NAME + JavaFileType.DOT_DEFAULT_EXTENSION;

	@Override
	public void actionPerformed(AnActionEvent event) {
		final PsiClass solutionClass = this.solutionClass;
		if (solutionClass == null) return;

		final Project project = solutionClass.getProject();

		final PsiManager manager = PsiManager.getInstance(project);
//		final GlobalSearchScope scope = GlobalSearchScope.allScope(project); // todo
		final JavaPsiFacade psiFacade = JavaPsiFacade.getInstance(project);
		final PsiElementFactory elementFactory = psiFacade.getElementFactory();
		final PsiJavaParserFacade parserFacade = psiFacade.getParserFacade();


//		ForwardDependenciesBuilder builder = new ForwardDependenciesBuilder(project, new AnalysisScope(project)); todo;


		final String path = JCHConfiguration.getInstance(project).getState().outputDirectory.replace(File.separatorChar, '/');
		ApplicationManager.getApplication().runWriteAction(new Runnable() {
			public void run() {
				try {
					PsiDirectory directory = DirectoryUtil.mkdirs(manager, path);
					PsiFile oldMainFile = directory.findFile(MAIN_CLASS_FILE_NAME);
					if (oldMainFile != null) oldMainFile.delete();

					PsiJavaFile mainClassFile = (PsiJavaFile)directory.createFile(MAIN_CLASS_FILE_NAME);

					PsiClass mainClass = (PsiClass)solutionClass.copy();
					mainClass.setName(MAIN_CLASS_NAME);

					mainClass.getExtendsList().delete();
					mainClass.getModifierList().findAnnotation(SolutionConfig.class.getName()).delete();
					PsiMethod mainMethod = elementFactory.createMethodFromText(generateMainMethodText(solutionClass), mainClass.getContext());
					mainClass.add(mainMethod);

					mainClass = (PsiClass)mainClassFile.add(mainClass);
					for (PsiClass aClass : ((PsiJavaFile)solutionClass.getContainingFile()).getClasses()) {
						if (!aClass.equals(solutionClass)) mainClassFile.add(aClass.copy());
					}

					ArrayList<String> inliningPackagesList = JCHConfiguration.getInstance(project).getState().inliningPackages;
					String[] inliningPackages = inliningPackagesList.toArray(new String[inliningPackagesList.size()]);

					dfs(mainClass, 0); // todo

					final Map<String, PsiClass> inlinedClasses = new HashMap<String, PsiClass>();
					Queue<PsiClass> classesToProcess = new LinkedList<PsiClass>();
					classesToProcess.add(mainClass);
					while (!classesToProcess.isEmpty()) {
						inlineReferences(elementFactory, parserFacade, mainClassFile, classesToProcess.poll(), inliningPackages, inlinedClasses, classesToProcess);
					}

//					for (PsiClass inlinedClass : inlinedClasses.values()) {
//						mainClassFile.add(inlinedClass);
//					}

//					for (Map.Entry<String, PsiClass> entry : inlinedClasses.entrySet()) {
//						PsiClass c = entry.getValue();
//						c.setName(entry.getKey().replace('.', '_'));
//					}


					// Remove unused code



					JavaCodeStyleManager.getInstance(project).shortenClassReferences(mainClassFile);
					JavaCodeStyleManager.getInstance(project).removeRedundantImports(mainClassFile);
					CodeStyleManager.getInstance(project).reformat(mainClassFile);
				} catch (Exception e) {
					ToolWindowManager.getInstance(project).notifyByBalloon(ToolWindowId.PROJECT_VIEW, MessageType.ERROR, e.getMessage());
				}
			}
		});
	}

	private void inlineReferences(PsiElementFactory elementFactory, PsiJavaParserFacade parserFacade, PsiFile containingFile, PsiElement element, String[] inliningPackages, Map<String, PsiClass> inlinedClasses, Queue<PsiClass> classesToProcess) {
		if (element instanceof PsiAnnotation) {
			element.delete();
		}

		if (element instanceof PsiJavaCodeReferenceElement) {
			PsiJavaCodeReferenceElement referenceElement = (PsiJavaCodeReferenceElement)element;
			PsiElement target = referenceElement.resolve();
			if (target instanceof PsiClass) {
				PsiClass targetClass = (PsiClass)target;
				String qualifiedName = targetClass.getQualifiedName();

				boolean isToInline = false;
				if (qualifiedName != null) {
					for (String packageName : inliningPackages) {
						if (qualifiedName.startsWith(packageName)) {
							isToInline = true;
							break;
						}
					}
				}
				if (isToInline) {
					PsiClass replacementClass = inlinedClasses.get(qualifiedName);
					if (replacementClass == null) {
						replacementClass = (PsiClass)targetClass.copy();
//						replacementClass.setName(qualifiedName.replace('.', '_'));
						// todo: inner classes...
						replacementClass.getModifierList().setModifierProperty(PsiModifier.PUBLIC, false);
						replacementClass.getModifierList().setModifierProperty(PsiModifier.STATIC, false);
						replacementClass = (PsiClass)containingFile.add(replacementClass);
						classesToProcess.add(replacementClass);
						inlinedClasses.put(qualifiedName, replacementClass);
					}

					referenceElement.bindToElement(replacementClass);

//					referenceElement.replace((PsiJavaCodeReferenceElement)replacementClass.getReference());
//					referenceElement.replace(replacementClass.getReference())
//					try {
//						PsiJavaCodeReferenceElement x = (PsiJavaCodeReferenceElement)referenceElement.getElement();
//						x.bindToElement(replacementClass);
//					} catch (ClassCastException e) {
//						e.printStackTrace();
//					}

//					elementFactory.createReferenceExpression(replacementClass);
//
//
//					PsiJavaCodeReferenceElement newReferenceElement = elementFactory.createClassReferenceElement(replacementClass);
//					referenceElement = (PsiJavaCodeReferenceElement)referenceElement.replace(newReferenceElement);

//					referenceElement.replace(parserFacade.createReferenceFromText(replacementClass.getName(), referenceElement.getContext()));


//					PsiJavaCodeReferenceElement newReferenceElement = parserFacade.createReferenceFromText(replacementClass.getName(), referenceElement.getContext());
//					referenceElement = (PsiJavaCodeReferenceElement)referenceElement.replace(elementFactory.createReferenceElementByFQClassName(replacementClass.getName(), GlobalSearchScope.allScope(element.getProject())));
//					referenceElement.bindToElement(referenceElement.resolve());
//					referenceElement.replace(elementFactory.createReferenceExpression(replacementClass));
//					referenceElement.bindToElement(replacementClass); // todo
				}
			}
		}
		for (PsiElement child : element.getChildren()) {
			inlineReferences(elementFactory, parserFacade, containingFile, child, inliningPackages, inlinedClasses, classesToProcess);
		}
	}

	private static void dfs(PsiElement element, int depth) { // todo: remove
		if (element instanceof PsiJavaCodeReferenceElement) {
			PsiJavaCodeReferenceElement referenceElement = (PsiJavaCodeReferenceElement)element;
			for (int i = 0; i < depth; i++) System.out.print("    ");
			System.out.println("########## " + referenceElement.getQualifiedName() + " " + referenceElement.resolve() + " " + referenceElement.getElement());
		}
		for (int i = 0; i < depth; i++) System.out.print("    ");
		System.out.println(element.toString());
		for (PsiElement child : element.getChildren()) {
			dfs(child, depth + 1);
		}
	}

	private static String generateMainMethodText(PsiClass solutionClass) {
		assert JCHUtil.isSolutionClass(solutionClass);
		PsiAnnotation solutionAnnotation = JCHUtil.getSolutionAnnotation(solutionClass);

		FileTemplateManager fileTemplateManager = FileTemplateManager.getInstance();
		FileTemplate mainMethodTemplate = fileTemplateManager.getCodeTemplate(JCHFileTemplateProvider.SOLUTION_MAIN_METHOD_TEMPLATE);

		String inputReaderClass = null;
		for (PsiClassType extendsClassType : solutionClass.getExtendsListTypes()) {
			if (extendsClassType.getCanonicalText().startsWith(Solution.class.getCanonicalName())) {
				inputReaderClass = extendsClassType.getParameters()[0].getCanonicalText();
				break;
			}
		}

		Properties properties = fileTemplateManager.getDefaultProperties();
		properties.setProperty("INPUT_READER", inputReaderClass);
		String inputValue = unquote(solutionAnnotation.findAttributeValue("input").getText());
		properties.setProperty("INPUT_INIT", inputValue.equals(SolutionConfig.STANDARD_INPUT) ? "java.lang.System.in" : "new java.io.FileInputStream(\"" + inputValue + "\")");
		String outputValue = unquote(solutionAnnotation.findAttributeValue("output").getText());
		properties.setProperty("OUTPUT_INIT", outputValue.equals(SolutionConfig.STANDARD_OUTPUT) ? "java.lang.System.out" : "new java.io.FileOutputStream(\"" + outputValue + "\")");
		try {
			return mainMethodTemplate.getText(properties);
		} catch (IOException e) {
			return null; // todo
		}
	}

	private static String unquote(String str) {
		int n = str.length();
		assert str.charAt(0) == '"';
		assert str.charAt(n - 1) == '"';
		return str.substring(1, n - 1);
	}
}
